Skip to content

Resizable panes, zoom, and a window that opens where it was left - #19

Merged
matt-edmondson merged 2 commits into
mainfrom
claude/auto-layout-expression-editor-ugcdvj
Sep 8, 2026
Merged

Resizable panes, zoom, and a window that opens where it was left#19
matt-edmondson merged 2 commits into
mainfrom
claude/auto-layout-expression-editor-ugcdvj

Conversation

@matt-edmondson

@matt-edmondson matt-edmondson commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Four changes to how the editor is arranged and looked at.

AstGraph.SeparateOverlaps is gone

ktsu.ForceDirectedLayout 3.18.0 resolves overlapping node boxes in the simulation itself (ktsu-dev/ImGuiApp#351), so the application was undoing overlaps the library had already undone. The method, its four constants, the editor's call to it and its two unit tests all go.

Editor_PullsOverlappingNodesApartAsItRuns stays, with its assertion rewritten to check the rectangles directly rather than to call the removed method. It is still worth having here: the separation only works when a node's measured size reaches the simulation, and that path — the renderer measuring, the editor writing the measurement into the engine — is this application's wiring, not the library's.

One comment was corrected rather than left: Separated()'s remark said two coincident nodes stay coincident for ever because repulsion has no direction between them. That was true when it was written and is not any more. The nudge is still worth doing — a node that appears exactly on top of the last one and then slides out is worse than one placed clear — so the code stays and the reason is now the real one.

The panes are divider containers

The layout is ktsu.ImGui.Widgets' DividerContainer, so the user drags the splits rather than living with the application's. A columns container holds the graph and a side column; the side column is a rows container holding properties on top and the code preview below.

Stacked rather than side by side because they are read at different times and want different shapes: properties are a short column of labelled rows, generated source is lines read downwards. Sharing one column gives each of them the full width and lets the user decide the height split.

AstGraphEditor.DrawInspector is now public so a host can place the panel itself; ShowInspector stays for a host that just wants an editor, and this application turns it off. The toolbar's Inspector checkbox is removed — with the panel a draggable pane, a checkbox is a second and worse way to do the same thing.

Zoom, and "Fit to canvas" that fits

The node editor underneath has no zoom of its own and reads each node's position straight out of the engine, so the only place a zoom can be applied is the engine. Applying it permanently would put the simulation in a space that changes whenever the slider moves — rest length, repulsion distance and overlap margin are all lengths, and none would mean the same thing afterwards.

So the scaling goes on before the frame is drawn and comes off after. Between those two calls the engine holds view positions, which is what gets drawn and what a drag is read back in; outside them it holds the graph's own, which is what the layout runs on. The font is scaled to match, because a node's box is sized from the text inside it — without that, zooming out would only pack the nodes closer while they stayed the same size.

Taking the transform off is not simply the inverse of putting it on, and getting that wrong is what the first attempt did: a size only arrives from the renderer when it has measured a new one, so dividing sizes unconditionally divided the same value again every frame, and within a second the nodes were thousands of times their real size — enough geometry to trip ImGui's 16-bit vertex-index assertion. A node the frame did not touch is now restored to exactly the value it had rather than divided.

Fit to canvas (the old "Fit") now picks the largest zoom the whole arrangement still fits at, with a margin, and never magnifies: a graph that already fits is shown at its own size, because magnifying it is not what "fit" means to someone who asked to see all of it. The first-frame fit also repeats until the nodes have been measured — the zoom it chooses depends on their sizes, and on frame one those are all still zero.

This part belongs in the library, and is on its way there. ktsu-dev/ImGuiApp#359 puts zoom and fit-to-view inside NodeEditorRenderer, where the transform can live at the seam between the engine and ImNodes instead of being wrapped around the whole frame from outside — and where the node editor's own padding and pin sizes can be scaled too, which from here they cannot. Once that ships, EnterViewSpace, LeaveViewSpace, Zoom and FitView come out of this application in favour of renderer.Zoom and renderer.FitToView. What is here is what can be done from outside the library today.

The window and the splits are remembered

Size, position and whether it was maximized go into EditorSettings alongside the recent files and the preview language, restored through ImGuiAppConfig.InitialWindowState and kept current by OnMoveOrResize. A first run leaves the position at the windowing layer's own "no position yet" sentinel, so the platform places the window rather than this application putting it in a corner.

The size and position stored are the ones the window has when it is not maximized, which is what should come back when a maximized window is restored.

This adds a Silk.NET.Windowing.Common reference, to name the maximized state — WindowState lives there rather than in the Silk.NET.Windowing facade beside it, which the second commit corrects after CI caught the first attempt with KTSU0006. That is the one thing here that costs a dependency; without it the size and position could persist but "was it maximized" could not.

Testing

329 tests, all passing (325 before, with 2 removed and 6 added). New coverage: fitting zooms out until an oversized graph fits, fitting does not magnify one that already fits, the zoom stays in range however it is set, the window and pane splits survive the settings round trip, and a first run leaves its position to the platform.

Verified visually through the headless harness: the three panes with their handles, the graph at 50% and 200%, and the graph returning to exactly its original size and place after zooming out, in, and back to 100%.

🤖 Generated with Claude Code

https://claude.ai/code/session_01QwzCFb8zhd263sZeRbF7ba

…t [minor]

Four changes to how the editor is arranged and looked at.

AstGraph.SeparateOverlaps is gone. ktsu.ForceDirectedLayout 3.18.0 does
that job in the simulation itself, so the application was undoing overlaps
the library had already resolved. The editor test that covered it stays,
asserting the rectangles directly: the separation only works when a node's
measured size reaches the simulation, and that wiring is still this
application's.

The panes are divider containers, so the user sizes them rather than the
application. Properties and the code preview are stacked in one column with
properties on top: they are read at different times and want different
shapes, and sharing a column gives each the full width and lets the user
decide the split. The editor no longer draws an inspector of its own here —
it still can, for a host that wants one — so the toolbar's Inspector
checkbox goes, the divider being a better way to do the same thing.

The graph can be zoomed, and "Fit to canvas" now zooms as well as centres.
The node editor underneath has no zoom, so the scaling is applied to the
engine's positions and the font together, put on before the frame is drawn
and taken off after: the simulation is never asked to work in a space that
changes under it, and a node dragged while zoomed lands where the pointer
was. Fitting picks the largest zoom the whole arrangement still fits at,
and never magnifies — a graph that already fits is shown at its own size.

The window and the pane splits are remembered between runs, alongside the
recent files and the preview language, since they are all things the user
arranged rather than settings the application should keep choosing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QwzCFb8zhd263sZeRbF7ba
KTSU0006: the maximized state is Silk.NET.Windowing.Common's, not
Silk.NET.Windowing's — the facade beside it. The reference was on the
wrong one of the two, so the type was still being used transitively.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QwzCFb8zhd263sZeRbF7ba
@sonarqubecloud

sonarqubecloud Bot commented Sep 8, 2026

Copy link
Copy Markdown

@matt-edmondson
matt-edmondson merged commit 6eb9d5c into main Sep 8, 2026
12 checks passed
@matt-edmondson
matt-edmondson deleted the claude/auto-layout-expression-editor-ugcdvj branch September 8, 2026 05:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants